Add production Express server (dist/ + dev/prod parity)#13
Conversation
Closes #12. Part of #7. - server.js: reuses src/api/index.js (sessions + API) + serves dist/ + custom 404 - src/api/index.js: shared Express app with client-sessions, /api/login, /api/logout, requireUser - src/api/404.js: terminal 404 handler for both dev (post-hook) and prod - package.json: adds express + client-sessions deps; changes start script to node server.js SESSION_SECRET must be set in production. Run with: npm run build && npm start Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| export default function notFound(req, res) { | ||
| res.statusCode = 404; | ||
| res.setHeader("content-type", "text/html"); | ||
| res.end(`<!DOCTYPE html> |
There was a problem hiding this comment.
Please put this in a separate 404.html file
There was a problem hiding this comment.
Done — moved the HTML into src/api/404.html and updated 404.js to read it from disk using readFileSync(new URL('404.html', import.meta.url), 'utf8') (ESM-compatible path resolution).
| }); | ||
|
|
||
| export function requireUser(req, res, next) { | ||
| if (!req.session?.user) return res.status(401).json({ error: "unauthorized" }); |
There was a problem hiding this comment.
Fixed — expanded into a braced block.
- Move inline HTML from 404.js into src/api/404.html - Read 404.html from disk via import.meta.url (ESM-compatible) - Expand one-line guard in requireUser into braced block Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| export default function notFound(req, res) { | ||
| res.statusCode = 404; | ||
| res.setHeader("content-type", "text/html"); | ||
| res.end(html); |
There was a problem hiding this comment.
Isn't there a send file function?
Or a way to use templated views to send it?
There was a problem hiding this comment.
Done — using res.sendFile(). Dropped the readFileSync pre-read; sendFile handles disk I/O, content-type, and ETag automatically.
| res.json({ ok: true }); | ||
| }); | ||
|
|
||
| export function requireUser(req, res, next) { |
There was a problem hiding this comment.
requireUser is a weird name. There's always a user. Maybe we mean requireLogin or similar?
- 404.html: proper 2-space indentation, self-closing <meta /> - 404.js: use res.status(404).sendFile() instead of readFileSync + res.end - index.js: rename requireUser → requireLogin (there's always a user) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Addressed all comments — pushed in one commit. Summary:
404.html: 2-space indentation, elements on separate lines,<meta />self-closing404.js: replacedreadFileSync+ raw Node response withres.status(404).sendFile()index.js:requireUser→requireLogin
| app.use(sessions({ | ||
| cookieName: "session", | ||
| secret: process.env.SESSION_SECRET, | ||
| duration: 24 * 60 * 60 * 1000, |
There was a problem hiding this comment.
Done — extracted ONE_DAY_MS and FIVE_MINUTES_MS constants above the app setup.
- Extract ONE_DAY_MS and FIVE_MINUTES_MS constants for session durations - Add GET /login route serving login.html (name field, no password) - Add path helpers to index.js (needed for sendFile) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
||
| app.use(express.static("dist")); | ||
| app.use(notFound); | ||
| app.listen(process.env.PORT || 3000); |
There was a problem hiding this comment.
| const __dirname = dirname(fileURLToPath(import.meta.url)); | ||
|
|
||
| export default function notFound(req, res) { | ||
| res.status(404).sendFile(join(__dirname, "404.html")); |
There was a problem hiding this comment.
Construct the path separately from sending the file
- server.js: replace app.listen() with createServer + promise-based startup (node:http pattern), logs URL on ready - src/api/404.js: extract filePath variable before calling sendFile Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Addressed both comments in the latest push:
|
| <button type="submit">Log In</button> | ||
| </form> | ||
| <script> | ||
| document.getElementById("login-form").addEventListener("submit", async (e) => { |
There was a problem hiding this comment.
Use semantic selectors
https://chriscalo.github.io/dev-skills/skills/web/dom/semantic-selectors
| const name = e.target.elements.name.value; | ||
| const res = await fetch("/api/login", { | ||
| method: "POST", | ||
| headers: { "content-type": "application/json" }, |
… selectors, form data
- server.js: wrap startup in listen() helper that returns { url, port, listener }
- src/api/404.js: hoist filePath to module level (computed once, not per request)
- src/api/login.html: use document.querySelector("form"), send URLSearchParams form data
- src/api/index.js: use express.urlencoded() to match form data submission
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Addressed all remaining comments in one commit:
|
There was a problem hiding this comment.
This is not following the pattern:
https://chriscalo.github.io/dev-skills/skills/nodejs/express/server-startup
Please fix. Use the listen() function verbatim.
| const __dirname = dirname(fileURLToPath(import.meta.url)); | ||
|
|
||
| export default function notFound(req, res) { | ||
| const filePath = join(__dirname, "404.html"); |
There was a problem hiding this comment.
Replace generic var name with something like notFoundHTML
And build the path once outside this function and reference in each execution
| res.json({ ok: true }); | ||
| }); | ||
|
|
||
| app.post("/api/logout", (req, res) => { |
There was a problem hiding this comment.
There needs to be a UI for logging out
- server.js: rewrite listen() verbatim from the server-startup pattern — nested startServer(), try/catch, EADDRINUSE retry with clearPort/delay; add DEVELOPMENT constant - src/api/404.js: rename filePath → notFoundPath - src/api/index.js: add GET /logout route serving logout.html - src/api/logout.html: new logout page with form that POSTs to /api/logout and redirects to /login on success Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Addressed all three comments in the latest push:
|
Summary
Closes #12. Part of #7.
server.js— production server: reuses the sharedappfromsrc/api/index.js(sessions + API already mounted), servesdist/as static files, falls through tonotFoundsrc/api/index.js— the single-source-of-truth Express app:client-sessionsmiddleware,/api/login,/api/logout,requireUserguard (prerequisite for Server composition: fractal api/ui modules (Strategy B build-watch) + promisified startup #8, Cookie auth with client-sessions (+ array-of-paths guard) #11)src/api/404.js— terminal 404 handler, used as the post-hook in dev (Server composition: fractal api/ui modules (Strategy B build-watch) + promisified startup #8, Add a custom 404 handler #10) and as the final middleware in prod (prerequisite for Add a custom 404 handler #10)package.json— addsexpress+client-sessionsto dependencies; replaceshttp-server diststart script withnode server.jsUsage
Notes on dependencies
src/api/index.jsandsrc/api/404.jsare created here becauseserver.jsimports them. They are also the prerequisite files for issues #8 (dev API), #10 (404 handler), and #11 (auth). Those issues can build on / refine these files rather than create them from scratch.Test plan
npm run buildcompletes without errorsSESSION_SECRET=test node server.jsstarts without errorscurl http://localhost:3000/returns the builtindex.htmlcurl http://localhost:3000/nonexistentreturns a 404 HTML responsecurl -X POST http://localhost:3000/api/login -H 'content-type: application/json' -d '{"name":"alice"}'returns{"ok":true}with aSet-Cookieheader🤖 Generated with Claude Code